<?php
/**
 * controller.class
 * 
 * MVC part. Controller class
 * 
 * @version:	0.2b
 * @package:	core
 * @author:	alex_ez
 * @created:	11 nov 2008
 *
 * @copyright:	(c) 2009 alex <qfox@ya.ru> yaroshevich aka alex_ez. All rights reserved.
 */

class controller_exception extends exception {};

abstract class basic_controller
{
	private $viewdata = null; // some data
	
	static private $__is = null; // instances
	static private $__page = null;
	static private $__active = array(); // active controllers
	static private $__action = null;

	protected $frame = '!main.tpl';
	protected $templatens = null;
	protected $crumbs = null;
	protected $basepath = null;
	
	protected $visitor = null;
	protected $visitor_roles = null;
	
	protected $url_patterns = array( );
	
	public $hooks;
	
	final public function __construct( )
	{
		$this->visitor = visitor::instance();
		$this->visitor_roles = $this->visitor->roles;
		
		$this->config = config::instance();
		
		$this->hooks = new Hooks( $this );

		if( is_null( self::$__page ) ) self::$__page = str_replace( '_controller', '', get_class( $this ) );

		$walker = $this->hooks->fire('load')->walker();
		if( !$walker->alive() )
			return;

		$this->crumbs = array( '/' => pick( $this->config->home, __('Frontend') ) );

		$tns = str_replace( '_controller', '', get_class( $this ) );
		$ns = $this->ns($this);
		if( $ns != 'public' ) {
			$tns = $ns . '_' . substr( $tns, 0, -1 - strlen($ns) );
			$cr = array(
				'public' => array(
					'text' => __('Frontend'),
					'mlink' => 'frontend',
					),
				'admin' => array(
					'text' => __('Backend'),
					'mlink' => 'backend',
					),
				);
			$root = $cr[$ns];
			$base = $this->base_url();
			$root['href'] = $base;
			$this->crumbs = array( $base => $root );
		}
		$this->templatens = $tns;
		$this->on_start();

		if (!isset($this->basepath)) {
			$this->basepath = str_replace( array( '///', '//' ), '/', '/' . $this->ns($this) . '/' . str_replace('_','/',str_replace(array('_admin_controller','_controller'),'',get_class($this))) . '/' );
		}
	}

	protected function ns($c=null) {
		if (is_null($c))
			$c = $this->instance_of_page();

		return ( strtolower(substr(str_replace('_controller','',get_class($this)),-5)) == 'admin' ) ? 'admin' : 'public';
	}

	protected function base_url( ) {
		$ns = $this->ns($this);
		return $ns == 'public' ? '/' : '/' . $ns . '/';
	}

	protected function user_base_url( $state = null ) {
		if (empty($state))
			$state = $this->visitor->user['state'];

		return $state == 'person' ? '/' : '/' . $state . '/';
	}

	/**
	 * Start event method
	 * 
	 * @deprecated
	 */
	protected function on_start( ) { /*throw new Exception( "You must describe on_start event for page controllerrrr." );*/ }

	/**
	 * Gets instance of needed controller
	 * 
	 * @param string $c controllers class name
	 * @return controller
	 */
	static protected function &__instance_of( $c = null )
	{
		throw new Exception( 'oops! method __instance_of deprecated!' );
		return self::instance_of( $c );
	}

	static public function &instance_of( $c = null )
	{
		if( is_null( $c ) ) return;
		if( is_null( self::$__is ) ) self::$__is = array( );
		
		$a = self::parse_action( (string)($c).'::index' );
		$c = $a['class'];
		
		if( isset( self::$__is[ $c ] ) )
			return self::$__is[ $c ];
		
		$class_exists = class_exists( $c ); //;!is_null( get_class_methods( $c ) );
		if( !$class_exists ) { $null = null; return $null; }
		
		self::$__is[ $c ] = new $c( );
		
		if( self::$__is[ $c ] instanceof controller ) {
			self::$__is[ $c ]->hooks->fire('initialize');
			return self::$__is[ $c ];
		}
		
		throw new Exception( "`$c' class is not extended by `controller' class." );
		unset( self::$__is[ $c ] );
		
		return;
	}

	static public function &instance_of_page( ) {
		return self::$__page;
	}

	static public function &instance_of_active( ) {
		return self::instance_of( self::active_controller() );
	}

	/**
	 * Finds page by url
	 *
	 * @param string $url
	 * @return array|null page if founded, null otherwise
	 */
	static public function find_page( $url )
	{
		$pages = self::get_pages( );
		if( is_integer( $url ) )
			return isset( $pages[ (int)$url ] ) ? $pages[ (int)$url ] : null;
		
		$is_dev = request::instance( ) ->is_developer( );
		
		$debug = array( );
		foreach( $pages as $v )
		if( $v['state'] !== 'off' )
		{
			$m = array( );
			$_r = ( $v['url']{0} == '@' )
				? ( preg_match( str_replace( '/$', '/?$', $v['url'] ), $url, $m ) )
				: ( $v['url'] == $url || rtrim($v['url'],'/') == $url );

			// debug
			if( $is_dev )
				$debug[ ( $v['url']{0} == '@' ) ? 'regs' : 'urls' ][] = &$v['url'];
			
			// searching
			if( !$_r ) continue;
			
			// normalizing path via redirect 301
			self::normalize_path( $url );
			
			// pages with dev state showed for developers (overpower)
			if( $v['state'] === 'dev' && !$is_dev ) continue;
			
			// parse action and fetch params of founded page and return them
			$_action = self::parse_action( $v['action'] );
			if( is_null( self::$__action ) ) self::$__action = $v['action'];

			$v[ 'class' ] = $_action['class'];
			$v[ 'method' ] = $_action['method'];
			
			if( !empty( $m ) ) array_shift( $m );
			$v[ 'params' ] = $m;

			return $v;
		}
		
		if( $is_dev )
		{
			sort($debug['urls']);
			if (bc::enabled()) bc::error( $url, 'url' );
			if (bc::enabled()) bc::error( $debug, 'pages' );
		}

		// null if page not found
		return null;
	}

	/**
	 * Finds ajax action by url
	 *
	 * @param string $url request::$path by default
	 * @return array|null ajax action if founded, null otherwise
	 */
	static public function find_ajax( $url = null )
	{
		$request = request::instance();
		
		$path = $request ->path;
		if( !is_null( $url ) )
		{
			$path = array_filter( explode( '/', $url ), create_function( '$a', 'return !!$a;' ) );
		}
		
		// remove ajax
		array_shift( $path );
		
		// determinate namespace
		$_ns = 'public';
		$cp = array( 'public' => '', 'admin' => '_admin' );
		if( $path[0] == 'admin' ) $_ns = array_shift( $path );
		$cp = $cp[$_ns];
		
		// class methods with higher priority than methods
		$c = $path;
		$m = array( );
		
		$v = array( );
		while( count( $c ) > 1 ) {
			// try to be deeper
			array_unshift( $m, array_pop( $c ) );
			$v[ 'class' ] = join( '_', $c ).$cp.'_controller';
			$v[ 'method' ] = 'ajax_' . join( '_', $m );
			$v[ 'action' ] = self::render_action( $v['class'], $v['method'] ); //'ajax::'.($_ns!='public'?"{$_ns}::":'')."{$c}::".strtolower( join( '_', $path ) );
			if( method_exists( $v['class'], $v['method'] ) ) break;
		}
		if( !method_exists( $v['class'], $v['method'] ) ) { if (bc::enabled()) bc::error( $v, 'not found ;(' ); return null; }
		
		self::normalize_path( $request ->raw_url );
		
		$v[ 'params' ] = $request ->data;
		
		return $v;
	}

	/**
	 * Redirect user to normalized url if not
	 * 
	 * @param string $url current url
	 */
	static public function normalize_path( $url )
	{
		if( mb_strlen( $url ) <= 2 ) return;
		if( !strpos( ' .php .xml', substr( $url, -4, 4 ) ) && substr( $url, -1, 1 ) != '/' )
			response::instance( ) ->moved( $url.'/'.(strlen($_SERVER['QUERY_STRING'])?('?'.$_SERVER['QUERY_STRING']):'') );
		if( strpos( ' .php/ .xml/', substr( $url, -5, 5 ) ) )
			response::instance( ) ->moved( rtrim($url,'/').(strlen($_SERVER['QUERY_STRING'])?('?'.$_SERVER['QUERY_STRING']):'') );
	}

	/**
	 * action parser
	 *
	 * @param string $action
	 * @return array
	 */
	static public function parse_action( $action )
	{
		if (!$action) return null;

		// todo: rewrite
		preg_match( '@^(?:(?P<special>ajax)(?:::)|)(?:(?P<ns>admin)(?:::)|)(?:(?P<controller>[a-z][a-z\_0-9]*?)(?:_controller)?|)::(?:page_|block_)?(?P<method>[a-z]{1}[a-z\_0-9]+)$@iu', $action, $n );
		if( $n['controller'] === '' ) $n['controller'] = self::get_page_controller();
		if( $n['special'] ) $n['method'] = $n['special'] . '_' . $n['method'];

		$controller_suffix = ($n['ns']=='admin')?'_admin':'';
		$controller_suffix .= '_controller';
		$r = array( 'class' => $n['controller'] . $controller_suffix, 'method' => $n['method'] );

		return $r;
	}

	/**
	 * action render
	 *
	 * @param string $controller
	 * @param string $method
	 * @return string
	 */
	static public function render_action( $controller, $method = '' )
	{
		$action = '';
		if( 0 === stripos( $method, 'ajax_' ) ) { $action .= 'ajax::'; $method = strtolower($method[4]).substr( $method, 5 ); }
		if( false !== stripos( $controller, '_admin_controller' ) ) $action .= 'admin::';
		// todo: CHECKUP
		$action .= preg_replace( '@_(admin_)?controller$@i', '', $controller ).'::';
		$action .= preg_replace( '@(page_|block_)$@i', '', $method );
		
		$action = rtrim( $action, ':' );
		
		return $action;
	}
	
	/**
	 * Gets main page controller classname
	 *
	 * @return string Owner controller classname
	 */
	static public function get_page_controller( )
	{
		return self::$__page;
	}


	static public function get_page_action( $k = null )
	{
		if (empty($k))
			return self::$__action;

		$a = controller::parse_action(self::$__action);
		return (isset($a[$k]) ? $a[$k] : null);
	}

	

	/**
	 * Gets all possible page urls
	 *
	 * @return array Array with page arrays
	 */
	static public function get_pages( )
	{
		$cache = cache::instance()->enable(__METHOD__,'2M');
		$cache_key = $cache->key(__CLASS__,__METHOD__);
		
		$r = $cache->$cache_key;
		if( false!==$r ) {
			$cache->disable(__METHOD__);
			return $r;
			}
		
		$pages = config::instance()->pages;
		
		// none by default
		$r = array();
		
		if ($pages == 'both' || $pages == 'base')
			$r = self::get_base_pages();
		if ($pages == 'both' || $pages == 'public')
			$r = array_merge( $r, self::get_public_pages() );
		
		$r = array_merge($r, self::get_admin_pages());
		usort( $r, create_function( '$a,$b', '$a=$a["priority"];$b=$b["priority"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
		
		$cache->$cache_key = $r;
		$cache->disable(__METHOD__);
		
		return $r;
	}

	/**
	 * Gets page urls from database
	 *
	 * @return array Array with page arrays
	 */
	static public function get_base_pages( )
	{
		$db = db::instance( );
		$_r = $db ->_list( "SELECT * FROM core_routes ORDER BY priority ASC, id ASC;" );
		
		return $_r;
	}

	/**
	 * Gets public pages urls in controllers
	 *
	 * @return array Array with page arrays
	 */
	static public function get_public_pages( )
	{
		$_r = array( );
		
		$globs = array();
		foreach( rad_base_paths() as $path )
			$globs = array_merge( $globs, glob( $path . 'controllers/*.php' ) );
		
		$classes = array( );
		$_public = strtolower(config::instance( )->public);
		//if (bc::enabled()) bc::log( $_public, '_public' );
		foreach( $globs as $c )
		{
			$class_name = pathinfo( $c, PATHINFO_FILENAME );
			// todo: rewrite this block to parser and analizer with wildcards or regexps
			if( $_public !== '*' && (strpos( $_public, strtolower($class_name) ) === false) ) continue;
			
			if( array_key_exists( $class_name, $classes ) ) continue;
			$classes[$class_name] = 0;
			$priority = strtolower($class_name) == 'index' ? 70 : 50;
			$methods = get_class_methods( $class_name.'_controller' );
			if( is_array( $methods ) )
			foreach( $methods as $m )
			{
			if( preg_match( '@^page_(?P<method>[a-z_]+)$@i', $m, $_m ) !== 1 ) continue;
			$classes[$class_name] ++;
			
			$method_name = $_m['method'];
			$action = $class_name . '::' . $method_name;
			$_page = array( 'state' => 'on', 'options' => '', 'priority' => $priority + ((strtolower($method_name) == 'index')?-50:10) );
			$_page['action'] = $action;
			$_page['url'] = strtolower( str_replace( '//', '/', '/'.trim( str_replace( array( '::', '_' ), '/', str_replace( array( 'index', '::index' ), '::', $action ) ), ' /').'/' ) );
			$_page['class'] = str_replace( realpath( root ).'/', '', $c );
			$_r[] = $_page;
			}
			else
				throw new controller_exception( 'Found invalid file "'.$c.'" in controllers path. It must contains class "'.$class_name.'_controller".' );
		}
		usort( $_r, create_function( '$a,$b', '$a=$a["priority"];$b=$b["priority"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
 		
		return $_r;
	}

	/**
	 * Gets admin pages urls in controllers
	 *
	 * @return array Array with page arrays
	 */
	static public function get_admin_pages( )
	{
		$_r = array( );
		
		$globs = array();
		foreach( rad_base_paths() as $path )
			$globs = array_merge( $globs, glob( $path . 'controllers/admin/*.php' ) );
		
		$classes = array( );
		foreach( $globs as $c )
		{
			$class_name = pathinfo( $c, PATHINFO_FILENAME );
			if( array_key_exists( $class_name, $classes ) ) continue;
			$classes[$class_name] = 0;
			$priority = strtolower($class_name) == 'index' ? 120 : 100;
			$methods = get_class_methods( $class_name.'_admin_controller' );
			if( is_array( $methods ) )
			foreach( $methods as $m )
			{
			if( preg_match( '@^page_(?P<method>[a-z_]+)$@i', $m, $_m ) !== 1 ) continue;
			$classes[$class_name] ++;
			
			$method_name = $_m['method'];
			$action = $class_name . '::' . $method_name;
			$_page = array( 'state' => 'on', 'options' => '', 'priority' => $priority + ((strtolower($method_name) == 'index')?-50:10) );
			$_page['action'] = 'admin::'.$action;
			$_page['url'] = '/admin'.strtolower( str_replace( '//', '/', '/'.trim( str_replace( array( '::', '_' ), '/', str_replace( array( 'index', '::index' ), '::', $action ) ), ' /').'/' ) );
			$_page['class'] = str_replace( realpath( root ).'/', '', $c );
			$_r[] = $_page;
			}
			else
				throw new controller_exception( 'Found invalid file "'.$c.'" in controllers path. It must contains class "'.$class_name.'_admin_controller".' );
		}
		
		usort( $_r, create_function( '$a,$b', '$a=$a["priority"];$b=$b["priority"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
		
		return $_r;
	}
	
	/**
	 * Gets public controllers
	 *
	 * @return array Array with public controllers names
	 */
	static public function get_public_controllers( ) {
		return self::get_controllers();
	}

	/**
	 * Gets admin controllers
	 *
	 * @return array Array with admin controllers names
	 */
	static public function get_admin_controllers( ) {
		return self::get_controllers('admin');
	}

	/**
	 * Gets abstract controllers list
	 *
	 */
	static protected function get_controllers( $sub = null )
	{
		$base = 'controllers' . (!is_null($sub) ? ('/'.(string)($sub)) : '' );

		$classes = array();
		$classes[] = 'index';

		foreach (array_reverse(rad_base_paths($base)) as $path) {
			$globs = glob($path . '*.php');
			usort ($globs, 'strnatcmp');
			foreach ($globs as $c)
				$classes[] = pathinfo($c, PATHINFO_FILENAME);
		}

		$classes = array_unique($classes);

		return $classes;
	}


	/**
	 * Unpacker for options
	 *
	 * @param string $o String with data in serialize or json or urlquery
	 * @return array|null
	 */
	static protected function unpack_options( $o )
	{
		if( ($_r = @unserialize( $o )) !== false )	return $_r;
		if( is_array( $_r = @json::decode( $o, 1 ) ) )	return $_r;
		
		parse_str( $o, $_r );
		return is_array( $_r ) ? $_r : null;
	}

	/**
	 * Packer for options
	 * 
	 * @param array $o
	 * @return string json encoded string
	 */
	static protected function pack_options( $o )
	{
		return json::encode( $o );
	}
	
	
	
	public function __set( $k, $v )
	{
		if( !$k || !is_string( $k ) ) return null;
		
		if( strpos( $k, '__' ) !== false )
		{
			$ak = explode( '__', $k );
			if( count( $ak ) != 2 ) return;
			$k = $ak[0];
			$v = array( $ak[1] => $v );
		}
		
		switch( $k )
		{
		case "meta":
		case "view":
			if(is_string($v))
				$v = self::unpack_options($v);
			if(!is_array($v))
				break;
			foreach($v as $vk => $vv)
				$this->viewdata[$k][$vk] = $vv;
		break;
		case "title":
			if(!is_string($v))
				$v = join(' — ', $v);
			$this->viewdata['title'] = $v;
			$this->viewdata['meta']['title'] = $v;
		break;
		default:
		$this->$k = $v;
		}
	}
	
	public function __get( $k )
	{
		return isset( $this ->viewdata[ $k ] ) ? $this ->viewdata[$k] : ( isset( $this ->$k ) ? $this ->$k : null );
	}
	
	public function __call( $_m, $p )
	{
		if( strpos( $_m, 'on' ) === 0 )
			return;

		if( strpos( $_m, '_ajax_' ) === 0 ) {
			$this->activate();
			$r = call_user_func_array(array($this,substr($_m,1)),$p);
			$this->deactivate();
			return $r;
		}

		if( isset( $p[1] ) && !is_array( $p[1] ) && strlen($p[1]) )
			$p[1] = self::unpack_options( $p[1] );
		if( !isset( $p[1] ) ) $p[1] = null;
		
		$view = view::instance();
		$__m = str_replace( array( 'page_', 'block_' ), '', $_m );
		$_r = null; $ob = '';

		$types = array( 'none', 'page', 'block' );
		$type = (strpos( $_m, 'page_' ) === 0) ? 1 : 2;
		$template = isset( $p[1]['template'] ) ? $p[1]['template'] : $__m.'.tpl';
		$templatens = isset( $p[1]['templatens'] ) ? $p[1]['templatens'] : $this->templatens;

		$this->hooks->fire( 'call', array( 'type' => $types[$type], 'type_code' => $type, 'method' => $__m ) );
		if( !$this->hooks->walker()->alive() )
			return (request::instance()->is_developer() ? ('<!-- Killed by: ' . json::encode(array( 'type' => $types[$type], 'type_code' => $type, 'method' => $__m )) . ' -->') : '');

		$this->activate();
		switch( $type ) {

		case 1:
		case 'page':

		$m = 'page_'.$__m;

		if( method_exists( $this, $m ) )
		{

			ob_start( );
			$__r = call_user_func_array( array( $this, $m ), $p );
			$ob = ob_get_contents(); ob_end_clean();
			
			if( is_array( $__r ) )
			{
				// metas
				if( !isset( $this ->meta['title'] ) && isset( $__r['title'] ) )
					$this ->title = $__r['title'];
				// template
				$template = isset( $__r['template'] ) ? $__r['template'] : $template;
				$templatens = isset( $__r['templatens'] ) ? $__r['templatens'] : $templatens;
				$__r['__url'] = $this->get_current_url( );
				$__r['url'] = request::instance()->url;
				// if( strtolower(substr( $__c, -5 )) == 'admin' ) { $__c = 'admin/'.substr( $__c, 0 ,-5 ); }
				
				$_r = $view->display( array(
					str_replace('_','/',$templatens).'/'.$template,
					$templatens.'/'.$template,
					str_replace('_','/',$templatens).'@'.$template,
					$templatens.'@'.$template,
					$this->ns($this).'/'.$template,
					$template
				), $__r );
			} else
				$_r = $__r;
			
			$_r = $ob.$_r;
			
			$this->hooks->fire( 'display', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
			
			$frame = (request::instance( )->g_noframe)
				? false
				: ( (is_array($__r)&&isset( $__r['frame'] ))
					? $__r['frame']
					: ( isset( $p[1]['frame'] )
						? $p[1]['frame']
						: $this ->frame ) );
			
			if( $frame )
			{
			$data = $this->viewdata;
			$data['url'] = request::instance()->url;
			$data['view']['controller'] = get_class( $this );
			$data['view']['method'] = $__m;
			$data['view']['action'] = self::render_action( get_class( $this ), $__m );
			$data['view']['params'] = $p[0];
			$data['center'] = $_r;

			$this->hooks->fire( 'frame', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
			$_r = $view->display( $frame, $data );
			}

			$this->hooks->fire( 'out', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
		}
		elseif( is_null( $_r ) )
		{
			if (bc::enabled()) bc::log( $_m." not found." );
			response::instance() ->notfound( );
		}
		break;
		
		case 2:
		case 'block':
		
		$m = 'block_'.$__m;
		if( method_exists( $this, $m ) )
		{
			ob_start( );
			$_r = call_user_func_array( array( $this, $m ), $p );
			$ob = ob_get_contents(); ob_end_clean();
			
			if( !is_string( $_r ) )
			{
				if( is_array( $_r ) )
				{
					if( isset( $_r['template'] ) ) $template = $_r['template'];
					if( isset( $_r['templatens'] ) ) $templatens = $_r['templatens'];
					$_r['__url'] = $this->get_current_url( );
				}
				// if( strtolower(substr( $__c, -5 )) == 'admin' ) { $__c = 'admin/'.substr( $__c, 0 ,-5 ); }
				$_r = $view ->display( array(
					str_replace('_','/',$templatens).'/_'.$template,
					str_replace('_','/',$templatens).'/'.$template,
					$templatens.'@_'.$template,
					$templatens.'@'.$template,
					'_'.$template,
					$template 
				), $_r );
			}
			
			$_r = $ob.$_r;

			$this ->hooks ->fire( 'display', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
		}
		break;
		
		default:
		$_r = "<!-- unknown type of {$__c}::{$_m} block -->";
		}
		
		$this->deactivate();
		
		return $_r;
	}

	/**
	 * activation/deactivation
	 */
	public function activate()
	{
		array_push(self::$__active,str_replace('_controller','',get_class($this)));
	}
	public function deactivate()
	{
		array_pop(self::$__active);
	}

	static public function active_controller()
	{
		return end(self::$__active);
	}
	static public function active_controllers()
	{
		return self::$__active;
	}



	
	public function get_crumbs( )
	{
		return $this->crumbs;
	}
	
	public function set_crumbs_of( controller $c )
	{
		if (bc::enabled()) bc::log( 'copied crumbs:', $c ->get_crumbs( ) );
		$this ->crumbs = $c ->get_crumbs( );
	}
	
	public function get_current_url( )
	{
		$z = $this->get_crumbs();
		end($z);
		$r = key($z);

		return trim(($r != '/' && $r != '/admin/')
			? $r
			: request::instance()->get_url_path(), '/');
	}
	
	public function get_inner_url( )
	{
		// fixme:
		$r = trim( str_replace( array( 'ajax/', 'admin/' ), '', request::instance( ) ->get_url( ) ), '/' );
		return substr( $r, strpos( $r, '/' ) );
	}
	
	public function block_crumbs( $p )
	{
		$p['delimiter'] = isset($p['delimiter'])&&strlen($p['delimiter'])
			? $p['delimiter']
			: '<li class="menu-delimiter"></li>';

		$p['current_url'] = !empty($p['current_url'])
			? $p['current_url']
			: $this->inner_url_path();

		$modes = array( 'root', 'current' );
		$p['mode'] = array_merge(
			array_fill_keys( $modes, false ),
			(array)( isset( $p['mode'] )
				? array_fill_keys( array_filter( explode( ' ', $p['mode'] ) ), true )
				: null )
			);

		$p['urlbase'] = !empty($p['urlbase'])
			? $p['urlbase']
			: $this->base_url();

		$items = array();
		foreach( $this->get_crumbs() as $href => $text ) {
			if (is_array($text)) {
				$el = $text;
				if (!isset($el['href']))
					$el['href'] = $href;

			} elseif (is_string($text)) {
				$el = compact( 'href', 'text' );

			} else
				continue;


			if (isset($el['href']) && is_numeric($el['href']))
				unset($el['href']);

			elseif ($el['href'] == '/' || $el['href'] == $p['urlbase'])
				$el['root'] = true;

			elseif (url_compare($p['current_url'],$el['href']))
				$el['current'] = true;

			$items[] = $el;
		}

		$r = $p;
		$r['items'] = $items;

		return $r;

		$r = array();
		foreach( $items as $href => $a ) {

//var_dump($this->inner_url_path(), $this->inner_url());die;
			if ($el['href'] == '/' && !$m['root'])
				continue;
			if (!$m['current'] && $p['current'] == $el['href'])
				continue;
//var_dump( $m, $p, $el ); echo "<br/><br/><br/><br/><br/>";

			$type = isset($el['type']) ? $el['type'] : '';
			unset($el['type']);
			if (empty($type))
				$type = (strlen($el['href']) && !is_numeric($el['href']) && (url_compare($p['current'],$el['href'])))
					? 'a' : 'span';

			$r[] = html_element( $type, $el );
		}
//			$_r[] = html_element( 'span', $p['current'] );
//			$_r[] = html_element( 'span', url_compare($p['current'],$el['href']) );
//	var_dump($p['current'], $el['href'], site_url($p['current']), site_url($el['href']), strcmp( site_url($p['current']), site_url($el['href']) ));die;
		
		return join( $delim, array_filter( $r ) );
	}
	
	public function page_index( $p, $o )
	{
		return 'Please create file index.php in ./application/controllers/ with class index_controller and method page_index.';
	}
	
	public function get_sitemap( ) { return array(); }
	
	
	
	
	
	
	
	static public function get_menu_items( ) { return null; }
	
	public function block_fullmenu( $p )
	{
		$cache = cache::instance()->enable('fullmenu','2M');
		$cache_key = $cache->key(__CLASS__,__METHOD__,array('roles'=>(string)($this->visitor_roles),'p'=>$p,));
		
		$r = $cache->$cache_key;
		if( false!==$r ) {
			$cache->disable('fullmenu');
			return $r;
			}
		
		if( !isset( $p['_'] ) || !is_array( $p['_'] ) ) {
			$p['_'] = array( );
			$p['_']['extends'] = array( 'controller', 'refbook_controller', 'refefence_controller' );
		}
		if( !isset($p['_']['suffix']) || empty($p['_']['suffix'])
			|| !isset($p['_']['listcallback']) || empty($p['_']['listcallback']) ) {
			$p['_']['suffix'] = '_controller';
			$p['_']['listcallback'] = 'get_public_controllers';
		}
		if( !isset( $p['_']['urlbase'] ) ) $p['_']['urlbase'] = '/';
		
		$_p = $p['_'];
		$mode = rtrim(_::pick( @$p['mode'], 'list' ),'s').'s'; //list, drop -> lists, drops
		
		$items = array( );
		$specials = array( );
		$listcallback = $_p['listcallback'];
		$list = (array)(self::$listcallback()); //(array)(call_user_func( 'controller::'.$_p['listcallback'] ));
		
		switch( $mode )
		{
		default:
		case 'lists':
			foreach( $list as $c )
			{
				$extends_checkup = 0;
				foreach ((array)($_p['extends']) as $class_extends) $extends_checkup += is_subclass_of( $c.$_p['suffix'], $class_extends );
				if( !$extends_checkup ) continue;

				$urls = (array)(call_user_func( $c.$_p['suffix'].'::get_menu_items' ));
				
				foreach( array( 'hide', 'regroup', 'rename', 'renamegroup', 'order' ) as $special )
				if( isset( $urls['@'.$special] ) ) { if( !isset( $specials[$special] ) ) $specials[$special] = array( ); $specials[$special] += $urls['@'.$special]; unset( $urls['@'.$special] ); }
				
				$item['order'] = (int)(isset( $item['order'] ) ? $item['order'] : 0);
				
				$items = array_merge( $items, $urls );
			}
			
			if( !empty($specials['hide']) && is_array($specials['hide']) ) {
				foreach( $specials['hide'] as $url ) {
					unset( $items[$url] );
				}
			}
			if( !empty($specials['rename']) && is_array($specials['rename']) ) {
				foreach( $specials['rename'] as $url => $newname ) {
					if( isset( $items[$url] ) )
						$items[$url] = $newname;
				}
			}
			uasort( $items, create_function( '$a,$b', '$a=@$a["order"];$b=@$b["order"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
			
			break;
		case 'drops':
			$items = $groups = array( );

			$last_section = null;
			$last_order = 0;
			foreach( $list as $c )
			{
				$extends_checkup = 0;
				foreach ((array)($_p['extends']) as $class_extends) $extends_checkup += is_subclass_of( $c.$_p['suffix'], $class_extends );
				if( !$extends_checkup ) continue;

				$urls = (array)(call_user_func( $c.$_p['suffix'].'::get_menu_items' ));

				//if ($c == 'goods_attributes_groups') $dump = compact( 'last_section', 'urls' );

				foreach( array( 'hide', 'regroup', 'rename', 'renamegroup', 'order' ) as $special )
				if( isset( $urls['@'.$special] ) ) { if( !isset( $specials[$special] ) ) $specials[$special] = array( ); $specials[$special] = array_merge( $specials[$special], $urls['@'.$special] ); unset( $urls['@'.$special] ); }

				foreach( $urls as $url => $item )
				{
					if( is_string($item) && ( $item == '@hyphen' || $item == '-' || $item == '—') || is_array($item) && ($item['text'] == '-' || $item['text'] == '—') ) {
						if (!is_array($item))
							$item = array();
						$item['text'] = '—';
						$url = '#hyphen'.rand(100,999);
					}
					if( is_string($item) )
						$item = array( 'text' => $item );
					elseif( !is_array( $item ) )
						continue;
					if( $item['text'] == '—' )
						$item['type'] = 'hyphen';

					$item['order'] = (int)( isset($item['order']) ? $item['order'] : $last_order + 10 );

					if (isset($item['section']) && !empty($item['section']))
						$section = $item['section'];
					elseif (isset($item['group']))
						$section = texo::run( $item['group'], 'stags latinate lowercase' );
					elseif( empty($item['section']) && empty($last_section) )
						$section = texo::run( $c, 'stags latinate lowercase' );
					else
						$section = $last_section;

					$last_section = $section;
					$last_order = $item['order'];

					$items[$section][$url] = $item;
					$groups[$section] = null;//_::pick(@$groups[$section],@$item['group'],$c);
				}
			}

			foreach (array_keys($groups) as $section) {
				foreach ($items[$section] as $item) {
					if (empty($item['group'])) continue;
					$groups[$section] = $item['group'];
					break;
				}
				if (empty($groups[$section]))
					$groups[$section] = __(ucfirst($section));
			}
//var_dump( __METHOD__, compact('list', 'groups', 'items', 'dump') );die;
//var_dump( $items, @$dump );
			
			// specials
			foreach( array_keys( $items ) as $group )
			foreach( array( 'hide', 'rename', 'regroup' ) as $sp_key )
			{
				if( !isset($specials[$sp_key]) || !is_array($specials[$sp_key]) ) continue;
				
				foreach( array_keys( $specials[$sp_key] ) as $url )
				{
				$new =& $specials[$sp_key][$url];
				
				switch( $sp_key )
				{
				case 'hide':
					unset( $items[$group][$new] );
					break;
				case 'rename':
					if( !isset( $items[$group][$url] ) ) break;
					$items[$group][$url]['text'] = $new;
					break;
				case 'regroup':
					if( !isset( $items[$group][$url] ) ) break;
					$items[$new][$url] = $items[$group][$url];
					unset($items[$group][$url]);
					break;
				}
				}
			}
			
			// cleanup
			foreach( array( 'hide', 'rename', 'regroup' ) as $sp_key )
			{
				if( !isset($specials[$sp_key]) || !is_array($specials[$sp_key]) ) continue;
				unset( $specials[$sp_key] );
			}
			
			// rename groups with save order
			if( isset($specials['renamegroup']) && is_array( $specials['renamegroup'] ) )
			{
			$_items = $items; $items = array( );
			foreach( $_items as $group => $urls )
				$items[_::pick( @$specials['renamegroup'][$group], $group )] = $urls;
			unset( $_items );
			}
			
			// group preparing
			foreach( $items as $group => $urls ) {
				// clearing of empty
				if( empty( $urls ) ) { unset( $items[$group] ); continue; }
				// ordering
				uasort( $items[$group], create_function( '$a,$b', '$a=@$a["order"];$b=@$b["order"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
			}
		}

		// group order
		if( isset( $specials['order'] ) && is_array( $specials['order'] ) && !empty( $items ) )
		{
			$_items = array( );
			$order = array_unique( $specials['order'] );
			ksort( $order );
			foreach( $order as $group )
			if( isset( $items[$group] ) )
			{
				$_items[$group] = $items[$group];
				unset( $items[$group] );
			}
			$_items += $items;
			$items = $_items;
		}
		
		//if (bc::enabled()) bc::log( $items, 'fullmenu' );
		//if (bc::enabled()) bc::log( $specials, 'specials' );
		$r = compact( 'mode', 'items', 'groups' );

		$r['template'] = pick( @$p['template'], '_fullmenu.tpl' );
		$r['urlbase'] = $p['_']['urlbase'];
		
		$cache->$cache_key = $r;
		$cache->disable('fullmenu');
		
		return $r;
	}



	/**
	 * 
	 */
	public function inner_url( $method = null, $params = null )
	{
		if (is_null($method)) {
			$action = $this->parse_action( $this->get_page_action() );
			$method = $action['method'];
			//var_dump( 'inner_url( null ): ', compact( 'method' ) );
		}

		if( isset($this->url_patterns[$method]) ) {
			$url = $this->url_patterns[$method];

		} else {
			$url = str_replace('::', '/', str_replace( array( '::index', '_', ), array( '', '/', ),
				$action = self::render_action( $this->basepath, $method )
				) );
			//if ($method !== 'remove' && $method != 'class') { var_dump( 'url_action', self::get_page_action(), compact( 'url', 'action', 'method', 'params' ) );die; }
		}

//var_dump( compact( 'url' ) );
//if (strpos( $url, 'goods//admin' )) { die; }

		if(is_numeric($params))
			$params = array( 'id' => $params );


		if( !empty($params) ) {
			if( is_array($params) && strpos($url,':')!==false )
				$url = subst($url,$params);
			elseif( (is_string($params)&&(strpos($params,'=')>strpos($params,'?') || (!strpos($params,'=') && !strpos($params,'?')))) )
				$url = $url . '/' . $params . '/';
			elseif( (is_string($params)&&strpos($params,'='))||is_array($params) )
				$url = $url . (strpos($url,'?')!==false ? '&' : '/?')
					. (is_array($params) ? http_build_query($params) : $params);
		}
		
		// normalize
		$url = str_replace(array('////','///','//'), '/', $url);
		$url = '/' . trim( $url, '/' ) . (strpos($url,'?')!==false ? '' : '/');

//var_dump( $method, $params, $url );
		
		return $url;
	}

	public function inner_url_path( $method = null )
	{
		$path = $this->inner_url( $method );

		if (strpos($path,'?')!==false)
			$path = substr($path,0,strpos($path,'?'));

//		var_dump( compact( 'path' ) );

		return $path;
	}
	
}
